iT邦幫忙

2022 iThome 鐵人賽

DAY 13
0

Reflect

什麼是反射
Go 語言提供了一種機制在運行時更新變數和檢查它們的值、調用它們的方法,但是在編譯時並不知道這些變數的具體類型,這稱為反射機制。

為什麼要用反射

需要反射的 2 個常見場景:

有時你需要編寫一個函數,但是並不知道傳給你的參數類型是什麼,可能是沒約定好;也可能是傳入的類型很多,這些類型並不能統一表示。這時反射就會用的上了。
有時候需要根據某些條件決定調用哪個函數,比如根據用戶的輸入來決定。這時就需要對函數和函數的參數進行反射,在運行期間動態地執行函數。
在講反射的原理以及如何用之前,還是說幾點不使用反射的理由:

與反射相關的代碼,經常是難以閱讀的。在軟體工程中,代碼可讀性也是一個非常重要的指標。
Go 語言作為一門靜態語言,編碼過程中,編譯器能提前發現一些類型錯誤,但是對於反射代碼是無能為力的。所以包含反射相關的代碼,很可能會運行很久,才會出錯,這時候經常是直接 panic,可能會造成嚴重的後果。
反射對性能影響比正常代碼運行速度慢一到兩個數量級。所以,對於一個項目中處於運行效率關鍵位置的代碼,盡量避免使用反射特性。

反射主要與Golang的interface相關(它的type是concrete type),只有interface才有反射一說。
在Golang的實現中,每個interface變數都有一個對應pair,pair中記錄了實際變數的值和類型:
(value, type)
復制代碼value是實際變量值,type是實際變數的類型。一個interface{}類型的變數包含了2個指針,一個指針指向值的類型【對應concrete type】,另外一個指針指向實際的值【對應value】。
例如,創建類型為*os.File的變數,然後將其賦給一個介面變數r:

tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)

var r io.Reader
r = tty

介面變數r的pair中將記錄如下信息:(tty, *os.File),這個pair在介面變數的連續賦值過程中是不變的,將介面變數r賦給另一個介面變數w:

var w io.Writer
w = r.(io.Writer)

介面變數w的pair與r的pair相同,都是:(tty, *os.File),即使w是空介面類型,pair也是不變的。
interface及其pair的存在,是Golang中實現反射的前提,理解了pair,就更容易理解反射。反射就是用來檢測存儲在介面變數內部(值value;類型concrete type) pair對的一種機制。

reflect的基本功能TypeOf和ValueOf
既然反射就是用來檢測存儲在介面變數內部(值value;類型concrete type) pair對的一種機制。那麼在Golang的reflect反射包中有什麼樣的方式可以讓我們直接獲取到變數內部的信息呢? 它提供了兩種類型(或者說兩個方法)讓我們可以很容易的訪問介面變數內容,分別是reflect.ValueOf() 和 reflect.TypeOf(),看看官方的解釋

// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i.  ValueOf(nil) returns the zero 
func ValueOf(i interface{}) Value {...}

翻譯一下:ValueOf用來獲取輸入參數介面中的數據的值,如果介面為空則返回0


// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {...}

翻譯一下:TypeOf用來動態獲取輸入參數介面中的值的類型,如果介面為空則返回nil

reflect.TypeOf()是獲取pair中的type,reflect.ValueOf()獲取pair中的value:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var num float64 = 1.2345

	fmt.Println("type: ", reflect.TypeOf(num))
	fmt.Println("value: ", reflect.ValueOf(num))
}

運行結果:
type: float64
value: 1.2345

reflect.TypeOf: 直接給到了我們想要的type類型,如float64、int、各種pointer、struct 等等真實的類型
reflect.ValueOf:直接給到了我們想要的具體的值,如1.2345這個具體數值,或者類似&{1 "Allen.Wu" 25} 這樣的結構體struct的值

也就是說明反射可以將“介面類型變量”轉換為“反射類型對象”,反射類型指的是reflect.Type和reflect.Value這兩種

從relfect.Value中獲取介面interface的信息
當執行reflect.ValueOf(interface)之後,就得到了一個類型為”relfect.Value”變數,可以通過它本身的Interface()方法獲得介面變數的真實內容,然後可以通過類型判斷進行轉換,轉換為原有真實類型。不過,我們可能是已知原有類型,也有可能是未知原有類型,因此,分兩種情況進行說明。

已知原有類型【進行“強制轉換”】
已知類型後轉換為其對應的類型的做法如下,直接通過Interface方法然後強制轉換,如下:
realValue := value.Interface().(已知的類型)

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var num float64 = 1.2345

	pointer := reflect.ValueOf(&num)
	value := reflect.ValueOf(num)

	// 可以理解為“強制轉換”,但是需要注意的時候,轉換的時候,如果轉換的類型不完全符合,則直接panic
	// Golang 對類型要求非常嚴格,類型一定要完全符合
	// 如下兩個,一個是*float64,一個是float64,如果弄混,則會panic
	convertPointer := pointer.Interface().(*float64)
	convertValue := value.Interface().(float64)

	fmt.Println(convertPointer)
	fmt.Println(convertValue)
}

運行結果:
0xc42000e238
1.2345
說明

轉換的時候,如果轉換的類型不完全符合,則直接panic,類型要求非常嚴格!
轉換的時候,要區分是指針還是指
也就是說反射可以將“反射類型對象”再重新轉換為“介面類型變量”

未知原有類型【遍歷探測其Filed】
很多情況下,我們可能並不知道其具體類型,那麼這個時候,該如何做呢?需要我們進行遍歷探測其Filed來得知,示例如下:

package main

import (
	"fmt"
	"reflect"
)

type User struct {
	Id   int
	Name string
	Age  int
}

func (u User) ReflectCallFunc() {
	fmt.Println("Allen.Wu ReflectCallFunc")
}

func main() {

	user := User{1, "Allen.Wu", 25}

	DoFiledAndMethod(user)

}

// 通過介面來獲取任意參數,然後一一揭曉
func DoFiledAndMethod(input interface{}) {

	getType := reflect.TypeOf(input)
	fmt.Println("get Type is :", getType.Name())

	getValue := reflect.ValueOf(input)
	fmt.Println("get all Fields is:", getValue)

	// 獲取方法欄位
	// 1. 先獲取interface的reflect.Type,然後通過NumField進行遍歷
	// 2. 再通過reflect.Type的Field獲取其Field
	// 3. 最後通過Field的Interface()得到對應的value
	for i := 0; i < getType.NumField(); i++ {
		field := getType.Field(i)
		value := getValue.Field(i).Interface()
		fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
	}

	// 獲取方法
	// 1. 先獲取interface的reflect.Type,然後通過.NumMethod進行遍歷
	for i := 0; i < getType.NumMethod(); i++ {
		m := getType.Method(i)
		fmt.Printf("%s: %v\n", m.Name, m.Type)
	}
}

運行結果:
get Type is : User
get all Fields is: {1 Allen.Wu 25}
Id: int = 1
Name: string = Allen.Wu
Age: int = 25
ReflectCallFunc: func(main.User)

通過運行結果可以得知獲取未知類型的interface的具體變數及其類型的步驟為:

先獲取interface的reflect.Type,然後通過NumField進行遍歷
再通過reflect.Type的Field獲取其Field
最後通過Field的Interface()得到對應的value

通過運行結果可以得知獲取未知類型的interface的所屬方法(函數)的步驟為:

先獲取interface的reflect.Type,然後通過NumMethod進行遍歷
再分別通過reflect.Type的Method獲取對應的真實的方法(函數)
最後對結果取其Name和Type得知具體的方法名
也就是說反射可以將“反射類型對象”再重新轉換為“介面類型變數”
struct 或者 struct 的嵌套都是一樣的判斷處理方式


上一篇
Day12.Graphql Schema,Type
下一篇
Day14.Apollo client
系列文
挑戰typescript+react+golang+graphql18
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言